Skip to content

feat: fix llm vidibility bug fix, article body appearence in server sent html#382

Merged
nehagup merged 3 commits intomainfrom
llm-visibility-bug-fix
May 1, 2026
Merged

feat: fix llm vidibility bug fix, article body appearence in server sent html#382
nehagup merged 3 commits intomainfrom
llm-visibility-bug-fix

Conversation

@amaan-bhati
Copy link
Copy Markdown
Member

@amaan-bhati amaan-bhati commented May 1, 2026

LLM Bot Visibility Fix - Blog Post Body SSR

The Bug

GPTBot, ClaudeBot, and PerplexityBot could not read Keploy blog post content.
When these crawlers fetched a post page, the <article> body contained only the
title, author, and a "No posts found matching ''" placeholder — zero prose.

Googlebot was unaffected because it executes JavaScript (since 2019) and waits
for hydration. LLM bots do not execute JavaScript; they parse the raw HTML
response and move on.


Root Cause Analysis

The PostBody component - which renders the entire article body — was imported
with ssr: false in both post page files:

const PostBody = dynamic(() => import("../../components/post-body"), {
  ssr: false,  // ← PostBody never rendered on the server
});

next/dynamic with ssr: false tells Next.js to skip server-side rendering for
that component entirely. The article content existed in the __NEXT_DATA__ JSON
blob (so Googlebot could hydrate it), but was never written into the HTML DOM
before JavaScript ran.

ssr: false was originally added because post-body.tsx called
document.createElement("textarea") synchronously inside renderCodeBlocks()
a browser-only API that would crash the Next.js build for any post containing
code blocks.


Fix

1. Make decodeHtmlEntities SSR-safe - components/post-body.tsx

Replaced the bare document.createElement call with a typeof document
guard plus a pure-JS fallback that covers all HTML entities WordPress generates:

const decodeHtmlEntities = (str: string): string => {
  if (typeof document !== "undefined") {
    const textarea = document.createElement("textarea");
    textarea.innerHTML = str;
    return textarea.value;
  }
  return str
    .replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">")
    .replace(/&quot;/g, '"').replace(/&#039;/g, "'").replace(/&#39;/g, "'")
    .replace(/&nbsp;/g, " ");
};

2. Remove ssr: false from PostBody imports

pages/technology/[slug].tsx and pages/community/[slug].tsx:

// Before
const PostBody = dynamic(() => import("../../components/post-body"), {
  ssr: false,
});

// After
const PostBody = dynamic(() => import("../../components/post-body"));

dynamic() is kept so the heavy PostBody bundle is still code-split. Only the
ssr: false flag is removed, unlocking server rendering.


Files Changed

File Change
components/post-body.tsx decodeHtmlEntities guarded for SSR; pure-JS fallback added
pages/technology/[slug].tsx ssr: false removed from PostBody dynamic import
pages/community/[slug].tsx ssr: false removed from PostBody dynamic import
tests/e2e/SeoMeta.spec.ts New regression test added (see Testing section)

Why Nothing Else Breaks

Every other browser-only API in PostBody is already safe:

  • All window / document calls (resize listener, IntersectionObserver, hash
    scroll, history) live inside useEffect — not called during SSR.
  • CodeMirror, AuthorCard, BlogSidebar, JsonDiffViewer each retain their
    own ssr: false — they render as null on the server and hydrate on the
    client. Code blocks appear as empty containers in SSR HTML; the prose around
    them is fully visible.
  • The TOC component uses createPortal(…, document.body) inside a {show && …} gate where show starts as falsedocument.body is never touched
    during SSR.
  • useState(content || "") gives SSR and the initial client render identical
    output. suppressHydrationWarning (already present on both content <div>s)
    covers the minor diff after the post-mount useEffect transforms the content.

Testing

Existing coverage (unchanged, all pass)

  • tests/components/PostBody.spec.ts - navigates to a real post in a browser,
    asserts [data-testid="post-content"] is visible, checks text length > 50
    characters, code blocks, copy buttons, TOC, author card.
  • tests/pages/TechnologyPostPage.spec.ts / CommunityPostPage.spec.ts
    click through to a post and assert PostBody renders.
  • All 36 Playwright E2E test files - none mock PostBody, none assert on SSR vs
    CSR behaviour. Zero breakage.
  • ran npm run build and npm run start, nothing breaks

New regression test - tests/e2e/SeoMeta.spec.ts

test('Post page article body should be in server-rendered HTML before JavaScript runs', async ({ request, baseURL }) => {
  const response = await request.get(`${baseURL!}/technology/understanding-api-testing-with-keploy`);
  expect(response.status()).toBe(200);

  const html = await response.text();

  // Strip __NEXT_DATA__ — it always contains the post JSON regardless of SSR.
  // Whatever remains must include the article prose for bots to see it.
  const htmlWithoutNextData = html.replace(
    /<script id="__NEXT_DATA__"[\s\S]*?<\/script>/i, ''
  );

  expect(htmlWithoutNextData).toContain('API testing');
});

This test uses Playwright's request API — an HTTP client with no browser and
no JavaScript execution — exactly replicating how GPTBot and ClaudeBot crawl a
page. It would have failed before this fix (prose only existed in the
stripped JSON blob) and now passes (prose is present in the SSR HTML).
TypeScript reports zero errors across the entire project after all changes.

Signed-off-by: amaan-bhati <amaanbhati49@gmail.com>
Copilot AI review requested due to automatic review settings May 1, 2026 10:55
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to make blog post article bodies visible in server-rendered HTML so non-JS crawlers (e.g., GPTBot/ClaudeBot) can read full post content, by enabling SSR for PostBody and making its HTML entity decoding SSR-safe.

Changes:

  • Made decodeHtmlEntities in components/post-body.tsx safe to run during SSR by guarding document usage and adding a string-replace fallback.
  • Removed ssr: false from the dynamic PostBody imports in the technology and community post pages to allow server rendering.
  • Added an E2E regression test intended to verify post body content exists in the raw SSR HTML response.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
components/post-body.tsx Adds SSR-safe HTML entity decoding to unblock server rendering.
pages/technology/[slug].tsx Enables SSR for PostBody by removing ssr: false from the dynamic import.
pages/community/[slug].tsx Enables SSR for PostBody by removing ssr: false from the dynamic import.
tests/e2e/SeoMeta.spec.ts Adds a regression test intended to validate SSR HTML contains post prose before JS executes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/e2e/SeoMeta.spec.ts Outdated
Signed-off-by: amaan-bhati <amaanbhati49@gmail.com>
@amaan-bhati amaan-bhati requested review from Copilot and removed request for Copilot May 1, 2026 11:21
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread components/post-body.tsx Outdated
Signed-off-by: amaan-bhati <amaanbhati49@gmail.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@nehagup nehagup merged commit 13cd415 into main May 1, 2026
8 checks passed
@nehagup nehagup deleted the llm-visibility-bug-fix branch May 1, 2026 14:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants